﻿using Ndns.ServerHost.Abstractions.Services;
using System;
using Ndns.Packet.Abstractions;
using Ndns.ServerHost.Abstractions;
using System.Collections.Concurrent;
using System.Threading;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Linq;

namespace Ndns.Cache
{
    class CacheCompactCallbackEventArgs : EventArgs
    {
        public int RemovalCount { get; set; }
        public int SurvivalCount { get; set; }

        public CacheCompactCallbackEventArgs(int removalCount, int survivalCount)
        {
            RemovalCount = removalCount;
            SurvivalCount = survivalCount;
        }
    }

    class DnsCacheServiceImpl : ICacheService
    {
        private readonly ConcurrentDictionary<DnsQueryKey, DnsCacheEntry> _entries;
        private readonly SemaphoreSlim _compactionSemaphore = new SemaphoreSlim(initialCount: 1, maxCount: 1);
        private bool _disposed;

        private ILogger _logger;

        private readonly int? _entryCountLimit;

        private TimeSpan _expirationScanFrequency;
        private DateTimeOffset _lastExpirationScan;

        private event EventHandler<CacheCompactCallbackEventArgs> _compactCallback;
        private event EventHandler<CacheCompactCallbackEventArgs> _expirationCallback;

        public DnsCacheServiceImpl(DnsCacheOptions options)
        {
            _entries = new ConcurrentDictionary<DnsQueryKey, DnsCacheEntry>();
            _expirationScanFrequency = options.ExpirationScanFrequency;
            _entryCountLimit = options.EntryCountLimit;
            _lastExpirationScan = DateTime.UtcNow;
        }

        public int Count => _entries.Count;

        public void RegisterServices(IServiceCollection services)
        {
            _logger = services.Get<ILoggerProvider>().GetLogger<DnsCacheServiceImpl>();
        }

        private DnsCacheEntry MakeEntry(DnsQueryKey key, DnsQueryAnswers answers, CachePriority priority)
        {
            var cacheAnswers = answers.Clone();
            uint? ttl = null;
            uint cTtl;
            foreach (var item in cacheAnswers.AnswerRRs)
            {
                cTtl = item.TTL;
                if (!ttl.HasValue || cTtl < ttl.Value)
                    ttl = cTtl;
            }
            foreach (var item in cacheAnswers.AuthoritativeNSs)
            {
                cTtl = item.TTL;
                if (!ttl.HasValue || cTtl < ttl.Value)
                    ttl = cTtl;
            }
            foreach (var item in cacheAnswers.AdditionalRRs)
            {
                cTtl = item.TTL;
                if (!ttl.HasValue || cTtl < ttl.Value)
                    ttl = cTtl;
            }
            if (!ttl.HasValue)
                return null;

            return new DnsCacheEntry(key, cacheAnswers, TimeSpan.FromSeconds(ttl.Value), priority);
        }
        private DnsQueryAnswers MakeAdjustedAnswers(DnsCacheEntry entry)
        {
            var answers = entry.Value.Clone();

            var deltaTtl = unchecked((uint)(DateTime.Now - entry.CreationDateTime).TotalSeconds);
            if (deltaTtl <= 0)
            {
                return null;
            }

            foreach (var item in answers.AnswerRRs)
            {
                item.TTL -= deltaTtl;
            }
            foreach (var item in answers.AuthoritativeNSs)
            {
                item.TTL -= deltaTtl;
            }
            foreach (var item in answers.AdditionalRRs)
            {
                item.TTL -= deltaTtl;
            }

            return answers;
        }

        public void Set(DnsHostName hostName, DnsRecordType t, DnsClass c, DnsQueryAnswers answers, CachePriority priority)
        {
            if (_disposed)
            {
                return;
            }

            var key = new DnsQueryKey(hostName, t, c);
            var entry = MakeEntry(key, answers, priority);

            do
            {
                if (entry == null)
                {
                    break;
                }

                if (_entryCountLimit.HasValue && _entries.Count >= _entryCountLimit.Value)
                {
                    TriggerOvercapacityCompaction();
                    break;
                }

                if (!_entries.TryGetValue(key, out var priorEntry))
                {
                    _entries.TryAdd(key, entry);
                }
                else
                {
                    if (!_entries.TryUpdate(key, entry, priorEntry))
                    {
                        _entries.TryAdd(key, entry);
                    }
                }

            } while (false);

            StartScanForExpiredItems();
        }
        public DnsQueryAnswers Get(DnsHostName hostName, DnsRecordType t, DnsClass c)
        {
            if (_disposed)
            {
                return null;
            }

            var key = new DnsQueryKey(hostName, t, c);

            if (!_entries.TryGetValue(key, out var entry))
            {
                return null;
            }

            if (entry.CheckExpired(DateTime.UtcNow))
            {
                return null;
            }

            entry.LastAccessDateTime = DateTime.UtcNow;
            var answers = MakeAdjustedAnswers(entry);
            if (answers == null)
            {
                return null;
            }

            return answers;
        }

        private void RemoveEntry(DnsCacheEntry entry)
        {
            if (_disposed)
            {
                return;
            }

            var entriesCollection = (ICollection<KeyValuePair<DnsQueryKey, DnsCacheEntry>>)_entries;
            entriesCollection.Remove(new KeyValuePair<DnsQueryKey, DnsCacheEntry>(entry.Key, entry));
        }

        private void StartScanForExpiredItems()
        {
            var now = DateTime.UtcNow;

            if (_expirationScanFrequency < now - _lastExpirationScan)
            {
                _lastExpirationScan = now;
                Task.Factory.StartNew(state => ScanForExpiredItems((DnsCacheServiceImpl)state), this,
                    CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
            }
        }
        private static void ScanForExpiredItems(DnsCacheServiceImpl cache)
        {
            var now = DateTime.UtcNow;
            int count = 0;
            foreach (var entry in cache._entries.Values)
            {
                if (entry.CheckExpired(now))
                {
                    cache.RemoveEntry(entry);
                    count++;
                }
            }

            int leftCount = cache._entries.Count;

            cache._expirationCallback?.Invoke(cache, new CacheCompactCallbackEventArgs(count, leftCount));
        }

        private void TriggerOvercapacityCompaction()
        {
            if (!_compactionSemaphore.Wait(0))
            {
                // Another compaction is running, exit immediately.
                // Avoid overpurging when multiple overcapacity compactions are triggered concurrently.
                return;
            }

            Task.Run(() =>
            {
                try
                {
                    // Compact 10%
                    Compact(0.10);
                }
                finally
                {
                    _compactionSemaphore.Release();
                }
            });
        }

        public void Compact(double percentage)
        {
            ScanForExpiredItems(this);

            int removalCountTarget = (int)(_entries.Count * percentage);

            var entriesToRemove = _entries.Select(e => e.Value).OrderBy(e => e.Priority).ThenBy(e => e.LastAccessDateTime).ThenBy(e => e.AbsoluteExpiration).Take(removalCountTarget);

            int count = 0;
            foreach (var item in entriesToRemove)
            {
                RemoveEntry(item);
                count++;
            }

            int leftCount = _entries.Count;

            _compactCallback?.Invoke(this, new CacheCompactCallbackEventArgs(count, leftCount));
        }

        public void AttachCompactCallback(CacheCompactCallback callback)
        {
            if (callback == null)
            {
                throw new ArgumentNullException(nameof(callback));
            }
            _compactCallback += (s, e) =>
            {
                try
                {
                    callback.Invoke(e.RemovalCount, e.SurvivalCount);
                }
                catch (Exception ex)
                {
                    _logger.LogError(string.Empty, "Compact callback threw an exception.", ex);
                }
            };
        }

        public void AttachExpirationCallback(CacheCompactCallback callback)
        {
            if (callback == null)
            {
                throw new ArgumentNullException(nameof(callback));
            }
            _expirationCallback += (s, e) =>
            {
                try
                {
                    callback.Invoke(e.RemovalCount, e.SurvivalCount);
                }
                catch (Exception ex)
                {
                    _logger.LogError(string.Empty, "Compact callback threw an exception.", ex);
                }
            };
        }

        #region IDisposable Support

        protected virtual void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                if (disposing)
                {
                    // 释放托管状态(托管对象)。
                }

                // 释放未托管的资源(未托管的对象)并在以下内容中替代终结器。
                // 将大型字段设置为 null。

                _disposed = true;
            }
        }

        // 仅当以上 Dispose(bool disposing) 拥有用于释放未托管资源的代码时才替代终结器。
        // ~CacheServiceImpl() {
        //   // 请勿更改此代码。将清理代码放入以上 Dispose(bool disposing) 中。
        //   Dispose(false);
        // }

        // 添加此代码以正确实现可处置模式。
        public void Dispose()
        {
            // 请勿更改此代码。将清理代码放入以上 Dispose(bool disposing) 中。
            Dispose(true);
            // 如果在以上内容中替代了终结器，则取消注释以下行。
            // GC.SuppressFinalize(this);
        }

        #endregion

    }
}
